package model;

import java.io.BufferedWriter;
import java.io.File;
import java.io.FileWriter;
import java.io.IOException;
import java.util.HashMap;
import java.util.HashSet;
import java.util.LinkedList;
import java.util.List;
import java.util.Map;
import java.util.PriorityQueue;
import java.util.Random;
import java.util.Set;

import javax.swing.text.html.MinimalHTMLWriter;

public class Model {
    private Set<Server> servers = new HashSet<Server>();
    private HashMap<String, Integer> degree = new HashMap<String, Integer>();
    public static Random RAND = new Random();
    public static int SLOTS = 20;
    public static final int S = 3;
    private Server prevServer;
    private Server firstServer;

    public static void main(String args[]) {
        int nodes = args.length > 0 ? Integer.parseInt(args[0]) : 10;
        int runs = args.length > 1 ? Integer.parseInt(args[1]) : 1000;
        int miniMeshes = args.length > 2 ? Integer.parseInt(args[2]) : 5;
        int usersPerMesh = args.length > 3 ? Integer.parseInt(args[3]) : 150;
        int success = 0;
        double avgMax = 0;
        double avgMin = 0;
        double totalAvgJumps = 0;
        int totalMaxJumps = 0;
        for (int j = 0; j < runs; j++) {
            Model m = new Model();
            for (int i = 0; i < nodes; i++) {
                // m.addServer();
                m.addServerFiveMin();
            }
            m.calcInDegree();
            if (m.isConnected())
                success++;
            avgMax += m.getMaxDegree();
            avgMin += m.getMinDegree();

            Map<Server, Integer> distances = distances(m.randomServer());
            int max = 0;
            int total = 0;
            for (Integer dist : distances.values()) {
                total = total + dist;
                if (dist > max) {
                    max = dist;
                }

            }

            totalAvgJumps += total / (double) nodes;
            totalMaxJumps += max;

            // add minimeshes
            for (int i = 0; i < miniMeshes; i++) {
                m.addProducer(String.valueOf((char) ('A' + i)));
            }
            for (int i = 0; i < miniMeshes; i++) {
                for (int l = 0; l < usersPerMesh; l++) {
                    m.addConsumer(String.valueOf((char) ('A' + i)));
                }
            }

            if(j==runs-1){
                dotGraph(m.firstServer,"oOut.dot","black");
                dotGraph(m.firstServer,"cOut.dot","transparent");
            }
        }

        System.out.printf("Nodes %4d: %4d / %4d (min: %f, max: %f)\n", nodes,
                success, runs, avgMin / runs, avgMax / runs);
        System.out.println("Average jumps: " + totalAvgJumps / runs
                + "  Average max jumps: " + totalMaxJumps / (double) runs);

    }

    private Server randomServer() {
        int index = RAND.nextInt(servers.size() - 1);
        int i = 0;
        for (Server serv : servers) {
            if (i == index) {
                return serv;
            }
            i++;
        }
        return null;
    }

    private void calcInDegree() {
        for (Server s : servers) {
            for (Server n : s.neighbors) {
                if (n != null) {
                    if (!degree.containsKey(n.name))
                        degree.put(n.name, 0);
                    degree.put(n.name, degree.get(n.name) + 1);
                }
            }
        }
    }

    private boolean isConnected() {
        return this.degree.size() == this.servers.size();
    }

    private int getMinDegree() {
        int min = Integer.MAX_VALUE;
        for (int temp : degree.values()) {
            if (temp < min)
                min = temp;
        }
        return min;
    }

    private int getMaxDegree() {
        int max = Integer.MIN_VALUE;
        for (int temp : degree.values()) {
            if (temp > max)
                max = temp;
        }
        return max;
    }

    public String toString() {
        StringBuilder sb = new StringBuilder();
        for (Server s : servers) {
            sb.append(s.toString());
            sb.append("\n");
        }
        return sb.toString();
    }

    private void addProducer(String miniMeshName) {
        Server firstContact = randomServer();
        Server toJoin = firstContact.getLeastLoadedNeighbor();
        toJoin.clientsAttached++;
        if(toJoin.clientsPerMesh.containsKey(miniMeshName)){
            toJoin.clientsPerMesh.put(miniMeshName, toJoin.clientsPerMesh.get(miniMeshName) +1 );
        }
        else{
            toJoin.clientsPerMesh.put(miniMeshName, 1);
        }
        toJoin.miniMeshes.put(miniMeshName, new Server[S]);
        toJoin.producersAttached.add(miniMeshName);
    }

    private void addConsumer(String miniMeshName) {
        Server firstContact = randomServer();
        Server toJoin = firstContact.getNeighborInMesh(miniMeshName);
        toJoin.clientsAttached++;
        if(toJoin.clientsPerMesh.containsKey(miniMeshName)){
            toJoin.clientsPerMesh.put(miniMeshName, toJoin.clientsPerMesh.get(miniMeshName) +1 );
        }
        else{
            toJoin.clientsPerMesh.put(miniMeshName, 1);
        }
        if (!toJoin.miniMeshes.containsKey(miniMeshName)) {
            addServerFiveMinMesh(toJoin, miniMeshName);
        }
    }

    private void addServerFiveMinMesh(Server s, String meshName) {
        Server[] meshNeighbors = new Server[S];
        s.miniMeshes.put(meshName, meshNeighbors);
        HashMap<Server, Integer> freq = new HashMap<Server, Integer>();

        boolean added = false;
        for (Server newServer : bfOrder(s)) {
            if (!newServer.miniMeshes.containsKey(meshName)) {
                continue;
            }
            for (Server x : newServer.miniMeshes.get(meshName)) {
                if (x == null)
                    continue;
                if (!freq.containsKey(x))
                    freq.put(x, 0);
                freq.put(x, freq.get(x) + 1);
            }
            added |= newServer.addMiniMeshNeighbor(s, meshName);
            s.addMiniMeshNeighbor(newServer, meshName);
        }
        while (!added) {
            for (Server newServer : bfOrder(prevServer)) {
                added |= newServer.addMiniMeshNeighbor(s, meshName);
            }
        }
        if (freq.size() > 0) {
            PriorityQueue<ServerRank> queue = new PriorityQueue<ServerRank>(
                    freq.size(), new ServerRank().new RankComparator());
            for (Server x : freq.keySet()) {
                ServerRank temp = new ServerRank();
                temp.s = x;
                temp.count = freq.get(x);
                queue.add(temp);
            }
            int i = 0;
            while (i < S && queue.peek() != null) {
                meshNeighbors[i] = queue.poll().s;
                i++;
            }
        }
    }

    private void addServerFiveMin() {
        Server s = new Server();
        servers.add(s);
        s.name = String.valueOf(servers.size());
        HashMap<Server, Integer> freq = new HashMap<Server, Integer>();
        if (prevServer != null) {
            boolean added = false;
            for (Server newServer : bfOrder(prevServer)) {
                for (Server x : newServer.neighbors) {
                    if (x == null)
                        continue;

                    if (!freq.containsKey(x))
                        freq.put(x, 0);
                    freq.put(x, freq.get(x) + 1);
                }
                added |= newServer.addNeighbor(s);
                s.addNeighbor(newServer);
            }
            while (!added) {
                for (Server newServer : bfOrder(prevServer)) {
                    added |= newServer.addNeighbor(s);
                }
            }
            if (freq.size() > 0) {
                PriorityQueue<ServerRank> queue = new PriorityQueue<ServerRank>(
                        freq.size(), new ServerRank().new RankComparator());
                for (Server x : freq.keySet()) {
                    ServerRank temp = new ServerRank();
                    temp.s = x;
                    temp.count = freq.get(x);
                    queue.add(temp);
                }
                int i = 0;
                while (i < S && queue.peek() != null) {
                    s.neighbors[i] = queue.poll().s;
                    i++;
                }
            }
        } else {
            firstServer = s;
        }
        prevServer = s;
    }

    private void addServer() {
        Server s = new Server();
        servers.add(s);
        s.name = String.valueOf(servers.size());
        if (prevServer != null) {
            for (Server newServer : bfOrder(prevServer)) {
                newServer.addNeighbor(s);
            }
            s.addNeighbor(prevServer);
            for (Server prevsSeen : prevServer.serversSeen) {
                s.addNeighbor(prevsSeen);
            }
        } else {
            firstServer = s;
        }
        prevServer = s;
    }

    private static Map<Server, Integer> distances(Server s) {
        Set<Server> marked = new HashSet<Server>();
        Map<Server, Integer> distance = new HashMap<Server, Integer>();
        LinkedList<Server> queue = new LinkedList<Server>();
        queue.add(s);
        marked.add(s);
        distance.put(s, 0);
        while (!queue.isEmpty()) {
            Server deq = queue.removeFirst();
            for (Server neighbor : deq.neighbors) {
                if (neighbor != null && !marked.contains(neighbor)) {
                    distance.put(neighbor, distance.get(deq) + 1);
                    queue.addLast(neighbor);
                    marked.add(neighbor);
                }

            }
        }
        return distance;
    }

    public static List<Server> bfOrder(Server s) {
        Set<Server> marked = new HashSet<Server>();
        LinkedList<Server> queue = new LinkedList<Server>();
        LinkedList<Server> ret = new LinkedList<Server>();
        queue.add(s);
        marked.add(s);
        while (!queue.isEmpty()) {
            Server deq = queue.removeFirst();
            for (Server neighbor : deq.neighbors) {
                if (neighbor != null && !marked.contains(neighbor)) {
                    queue.addLast(neighbor);
                    marked.add(neighbor);
                }

            }
            ret.addLast(deq);
        }
        return ret;
    }

    private static String stringToColor(String s) {
        if (s.equals("A")) {
            return "red";
        }
        if (s.equals("B")) {
            return "blue";
        }
        if (s.equals("C")) {
            return "forestgreen";
        }
        if (s.equals("D")) {
            return "yellow";
        }
        return "black";
    }

    private static void addEdge(Map<String, StringBuilder> m, String edge,
            String color) {
        if (!m.containsKey(edge)) {
            m.put(edge, new StringBuilder());
        }
        StringBuilder sb = m.get(edge);
        if (sb.length() == 0) {
            sb.append(color);
        } else {
            sb.append(":");
            sb.append(color);
        }
    }

    private static String dotGraph(Server s, String fileName, String oColor) {
        Map<String, StringBuilder> edges = new HashMap<String, StringBuilder>();
        StringBuilder sb = new StringBuilder();
        sb.append("digraph G { \n");
        Set<Server> marked = new HashSet<Server>();
        LinkedList<Server> queue = new LinkedList<Server>();
        queue.add(s);
        marked.add(s);
        while (!queue.isEmpty()) {
            Server deq = queue.removeFirst();
            for (String producer : deq.producersAttached) {
                addEdge(edges, producer + " -> " + deq.name,
                        stringToColor(producer));
                sb.append("    " + producer + " [shape=triangle, color="
                        + stringToColor(producer) + "]\n");
            }

            for (Server neighbor : deq.neighbors) {
                addEdge(edges, deq.name + " -> " + neighbor.name, oColor);
                if (neighbor != null && !marked.contains(neighbor)) {
                    queue.addLast(neighbor);
                    marked.add(neighbor);
                }
            }
            StringBuilder label = new StringBuilder();
            String color = "";
            for (String meshName : deq.miniMeshes.keySet()) {
                if(color.equals("")){
                    color = stringToColor(meshName);
                    label.append(meshName + "  " + deq.clientsPerMesh.get(meshName));
                }
                else{
                label.append("      " + meshName + "  " + deq.clientsPerMesh.get(meshName));
                }
                for (Server meshNeighbor : deq.miniMeshes.get(meshName)) {
                    if (meshNeighbor != null) {
                        addEdge(edges, deq.name + " -> " + meshNeighbor.name,
                                stringToColor(meshName));
                    }
                }
            }
            

            String labelString = label.toString();
            if (!labelString.equals("")) {
                sb.append("    " + deq.name + " [color = " + color + ", label = \""
                        + labelString + "\"]\n");
            }
        }
        for (String edgeName : edges.keySet()) {
            sb.append("    " + edgeName + " [color =\""
                    + edges.get(edgeName).toString() + "\"] \n");
        }
        sb.append("}");
        try {
            FileWriter fw = new FileWriter(fileName);
            BufferedWriter out = new BufferedWriter(fw);
            out.write(sb.toString());
            out.close();
            fw.close();
        } catch (IOException e) {
            // TODO Auto-generated catch block
            e.printStackTrace();
        }
       
        // try {
        // String cmd =
        // "\"C:\\Program Files (x86)\\Graphviz 2.28\\bin\\dot\" -Tpng \"" +
        // f.getAbsolutePath() + "\" > \"" + fOut.getAbsolutePath() + "\"";
        // System.out.println(cmd);
        // Runtime.getRuntime().exec(cmd);
        // } catch (IOException e) {
        // // TODO Auto-generated catch block
        // e.printStackTrace();
        // }
        return sb.toString();
    }
}
